﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Upreader.Common
{
    /// <summary>
    /// A memory stream where data is kept in constant chunks to prevent LOH framentation.
    /// </summary>
    public class ChunkedMemoryStream : Stream
    {
        #region Configuration

        // DefaultChunkSize of 85000. This will ensure to create each chunk on the LOH 
        // with a constant size to prevent fragmentation. Possibly, we might add a pool here
        public const int DefaultChunkSize = 85000;

        #endregion // Configuration

        #region Fields

        // use a linkedList to store the chunks
        private readonly List<byte[]> _chunks;
        private readonly int _chunkSize;
        private int _currentChunk;
        private int _currentChunkOffset;
        private long _length;

        #endregion // Fields

        #region Constructor

        public ChunkedMemoryStream()
            : this(DefaultChunkSize)
        {
        }

        public ChunkedMemoryStream(int chunkSize)
        {
            if (chunkSize <= 0)
                throw new ArgumentOutOfRangeException("chunkSize");

            _chunkSize = chunkSize;
            _chunks = new List<byte[]>();
        }

        #endregion // Constructor

        #region Properties

        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }

        public override long Length
        {
            get 
            {
                return _length;
            }
        }

        public override long Position
        {
            get
            {
                return (_chunkSize * _currentChunk) + _currentChunkOffset;
            }
            set
            {
                if (value == 0)
                {
                    _currentChunk = 0;
                    _currentChunkOffset = 0;
                }
                else
                {
                    _currentChunk = (int)(value / _chunkSize);
                    _currentChunkOffset = (int)(value - (_currentChunk * _chunkSize));
                }

            }
        } 

        public long Capacity
        {
            get { return _chunkSize * _chunks.Count; }
        }

        public int ChunkSize
        {
            get { return _chunkSize; }
        }

        protected List<byte[]> Chunks
        {
            get { return _chunks; }
        }

        protected int CurrentChunk
        {
            get { return _currentChunk; }
            set { _currentChunk = value; }
        }

        protected int CurrentChunkOffset
        {
            get { return _currentChunkOffset; }
            set { _currentChunkOffset = value; }
        }

        #endregion // Properties

        #region Methods

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (count > buffer.Length - offset)
                throw new ArgumentOutOfRangeException("count", "to many bytes were requested for the current buffer when starting from offset");

            int total = 0;

            while (true)
            {
                int currentChunkSize = _chunkSize - _currentChunkOffset;

                int bytesToRead = Math.Min(currentChunkSize, count);

                Buffer.BlockCopy(_chunks[_currentChunk],
                    _currentChunkOffset,
                    buffer,
                    offset,
                    bytesToRead);

                total += bytesToRead;
                offset += bytesToRead;
                count -= bytesToRead;

                if (count == 0)
                {
                    _currentChunkOffset += bytesToRead;
                    break;
                }
                else
                {
                    if (_chunks.Count == _currentChunk + 1)
                    {
                        // no more chunks to read from
                        break;
                    }
                    // we've reached the current's chunk capacity, move to the next chunked
                    _currentChunk++;
                    _currentChunkOffset = 0;
                }
            }

            return total;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            long newPos = 0;

            switch (origin)
            {
                case SeekOrigin.Begin:
                    newPos = offset;
                    break;
                case SeekOrigin.Current:
                    newPos = Position + offset;
                    break;
                case SeekOrigin.End:
                    newPos = Length - offset;
                    break;
            }

            Position = Math.Max(0, Math.Min(newPos, Length));
            return newPos;
        }

        public override void SetLength(long value)
        {
            PrepareCapacity(value);
            _length = value;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (count > buffer.Length - offset)
                throw new ArgumentOutOfRangeException("count", "to many bytes were requested for the current buffer when starting from offset");

            int currentCapacity = _chunkSize * _chunks.Count;
            int desiredCapacity = currentCapacity + count;
            PrepareCapacity(desiredCapacity);

            _length += count;

            int finalOffset = offset + count;
            while (true)
            {
                int currentChunkSize = _chunkSize - _currentChunkOffset;

                int bytesToWrite = Math.Min(currentChunkSize, count);

                Buffer.BlockCopy(buffer, 
                    offset, 
                    _chunks[_currentChunk],
                    _currentChunkOffset,
                    bytesToWrite);

                _currentChunkOffset += bytesToWrite;

                offset += bytesToWrite;
                count -= bytesToWrite;

                if (count == 0)
                {
                    break;
                }
                else
                {
                    // we've reached the current's chunk capacity, move to the next chunked
                    _currentChunk++;
                    _currentChunkOffset = 0;
                }
            }
        }

        public override void Flush()
        {
        }

        #endregion // Methods

        #region Helpers

        protected void PrepareCapacity(long total)
        {
            long currentCapacity = _chunks.Sum(x => x.Length);
            while (currentCapacity < total)
            {
                byte[] nextChunk = new byte[_chunkSize];
                _chunks.Add(nextChunk);

                currentCapacity += _chunkSize;
            }
        }

        #endregion // Helpers
    } 


}
